package code.google.jcontainer;

import code.google.jcontainer.annotation.Container;
import code.google.jcontainer.annotation.Resolve;
import code.google.jcontainer.event.ComponentInvocationEvent;
import code.google.jcontainer.event.ContainerListener;
import code.google.jcontainer.event.EventManager;
import code.google.jcontainer.invoke.Invocation;
import code.google.jcontainer.invoke.InvocationHandler;
import code.google.jcontainer.invoke.InvocationRuntimeException;
import code.google.jcontainer.resolver.AnnotationResolverRepository;
import code.google.jcontainer.resolver.ClassResolveContext;
import code.google.jcontainer.resolver.FieldResolveContext;
import code.google.jcontainer.resolver.MethodResolveContext;
import code.google.jcontainer.support.inject.Inject;
import code.google.jcontainer.support.intercept.Intercept;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Super Container class, provides basic methods for a Container class. <br>
 * Any real container should extend this class and annotated with @Container <br>
 * <p>
 * for example: <br>
 * <pre>
 * &#64;Container(
 *       name = "SimpleContainer",
 *       //<i> the classes annotated with this annotation will be managed by this container </i>
 *       componentType = SimpleBean.class,
 *       //<i>Optional, annotations can be resolved by this container</i>
 *       supportAnnotations = {IntegerConstraint.class, CountConsuming.class}
 *       //<i>Optional, third party annotations want to resolve</i>
 *       resolveAnnotations = {
 *               &#64;Resolve(
 *                       annotationClass = WebMethod.class,
 *                       resolverClass = WebMethodAnnotationResolver.class,
 *                       when = {Resolve.WHEN.AFTER_CONSTRUCT})
 *       }
 * )
 * public class SimpleContainer extends AbstractContainer{
 *
 *  //<i> a very simple example, do not need any methods</i>
 *
 * }
 * </pre>
 * </p>
 * @author <a href="mailto:jfox.young@gmail.com">Young Yang</a>
 */
public abstract class AbstractContainer {

    /**
     * container name, used for ContainerFactory.getContainer
     */
    private String name;

    /**
     * The \@Container annotation, used to init container
     */
    private Container containerAnnotation;

    /**
     * The annotation type the container managed
     */
    private Class<? extends Annotation> componentType;

    // annotation repository
    protected AnnotationRepository annotationRepository;

    // annotation resolver repository
    protected AnnotationResolverRepository annotationResolverRepository;

    //event manager
    protected EventManager eventManager;

    // all loaded components
    protected Map<String, ComponentMeta> components = new HashMap<String, ComponentMeta>();

    // invocation handles, will invoke one by one as a chain
    protected List<InvocationHandler> handlers = new ArrayList<InvocationHandler>();

    protected Log log = LogFactory.getLog(this.getClass());

    private boolean inited = false;

    public AbstractContainer() {
        this.containerAnnotation = this.getClass().getAnnotation(Container.class);
        this.name = containerAnnotation.name();
        this.componentType = containerAnnotation.componentType();
    }

    /**
     * init container, parse @Container, init InvocationHandlers, load components
     * load components
     *
     * @param annotationRepository
     */
    protected synchronized void init(AnnotationRepository annotationRepository) {
        if(inited) {
            return;
        }
        log.info("Initializing container: " + name);
        this.annotationResolverRepository = new AnnotationResolverRepository(name);
        this.annotationRepository = annotationRepository;
        this.eventManager = new EventManager(this.name);
        initSupportAnnotations();
        initInvocationHandlers();
        loadComponents(annotationRepository.getAnnotatedClasses(getComponentType()));
        log.info("Initialized container: " + name);
        inited = true;
    }

    /**
     * return container name
     */
    public String getName() {
        return name;
    }

    /**
     * init support annotations, it can be specified by {@link code.google.jcontainer.annotation.Container#supportAnnotations()}
     */
    protected void initSupportAnnotations() {
        // default embedded annotations
        annotationResolverRepository.addSupportAnnotationClass(code.google.jcontainer.support.log.Log.class);
        annotationResolverRepository.addSupportAnnotationClass(Intercept.class);
        annotationResolverRepository.addSupportAnnotationClass(Inject.class);
        // component annotation needed to be extra resolve
        if(componentType.isAnnotationPresent(Resolve.class)){
            annotationResolverRepository.addSupportAnnotationClass(componentType);
        }
        // support annotations in @Container
        for (Class<? extends Annotation> supportAnnotationClass : containerAnnotation.supportAnnotations()) {
            annotationResolverRepository.addSupportAnnotationClass(supportAnnotationClass);
        }

        // init resolveAnnotations
        for(Resolve resolveAnnotation : containerAnnotation.resolveAnnotations()){
            annotationResolverRepository.addResolveAnnotation(resolveAnnotation);
        }

    }

    /**
     * init invocation handlers, it can be specified by {@link code.google.jcontainer.annotation.Container#invocationHandlers()}
     */
    protected void initInvocationHandlers() {
        Class<? extends InvocationHandler>[] handleClasses = containerAnnotation.invocationHandlers();
        for(Class< ? extends InvocationHandler> handleClass : handleClasses){
            try {
                handlers.add(handleClass.newInstance());
            }
            catch (Exception e) {
                throw new ContainerRuntimeException(e);
            }
        }
    }

    /**
     * load and init components
     * @param componentClasses
     */
    protected void loadComponents(Class[] componentClasses) {
        for (Class<?> componentClass : componentClasses) {
            Annotation componentAnnotation = componentClass.getAnnotation(getComponentType());
            ComponentMeta componentMeta = new ComponentMeta(componentAnnotation, componentClass);
            components.put(componentMeta.getName(), componentMeta);
        }

        // loop all components and construct
        for(Map.Entry<String, ComponentMeta> entry : components.entrySet()){
            ComponentMeta componentMeta = entry.getValue();
            Class componentClass = componentMeta.getComponentClass();
            // instantiate components
            beforeConstructComponent(componentMeta.getName(), componentMeta.getComponentClass());
            Object componentObj = newComponent(componentClass);
            afterConstructComponent(componentMeta.getName(), componentMeta.getComponentClass(), componentObj);
            componentMeta.setComponentObject(componentObj);
            log.info("Loaded component: " + componentClass.getName());
        }

    }

    /**
     * return the managed annotation type
     */
    public Class<? extends Annotation> getComponentType() {
        return componentType;
    }

    /**
     * Determine if the container has such component
     */
    public boolean hasComponent(String componentName) {
        return components.containsKey(componentName);
    }

    /**
     * return annotation resolver repository, you may need to used it in some AnnotationResolver classes
     */
    public AnnotationResolverRepository getAnnotationResolverRepository() {
        return annotationResolverRepository;
    }

    /**
     * return annotation repository, used to determine if an annotation is loaded
     */
    public AnnotationRepository getAnnotationRepository() {
        return annotationRepository;
    }

    /**
     * return EventManager, used to fire event
     */
    public EventManager getEventManager() {
        return eventManager;
    }

    /**
     * Add listener
     */
    public void addListener(ContainerListener containerListener) {
        eventManager.addListener(containerListener);
    }

    /**
     * remove listener
     */
    public void removeListener(ContainerListener containerListener) {
        eventManager.removeListener(containerListener);
    }

    /**
     * instantiate a component, will be cached in ComponentMeta once instantiated
     */
    protected Object newComponent(Class<?> componentClass) {
        try {
            return componentClass.newInstance();
        }
        catch (Exception e) {
            throw new ContainerRuntimeException(e);
        }
    }

    /**
     * get a component by Class, return the first found component which implements this Class
     * it is useful for injecting by type
     * @return component or null
     */
    public final Object getComponent(Class<?> componentClass) {
        for(Map.Entry<String, ComponentMeta> entry : components.entrySet()) {
            String compName = entry.getKey();
            ComponentMeta compMeta = entry.getValue();
            if(compMeta.getComponentClass().isAssignableFrom(componentClass)){
                // return the first component extends this specified componentClass
                return getComponent(compName);
            }
        }
        return null;
    }

    /**
     * get all components by given component class
     */
    public final Object[] getComponents(Class<?> componentClass) {
        List<Object> allComponents = new ArrayList<Object>();
        for(Map.Entry<String, ComponentMeta> entry : components.entrySet()) {
            String compName = entry.getKey();
            ComponentMeta compMeta = entry.getValue();
            if(compMeta.getComponentClass().isAssignableFrom(componentClass)){
                // return the first component extends this specified componentClass
                allComponents.add(getComponent(compName));
            }
        }
        return allComponents.toArray();
    }

    /**
     * return component instance by compnent name
     * Default, return a CGLIB enhancered proxy object;
     * if @Container.useCGLIG is false, return a dynamic proxy object constructed by component's interfaces.
     *
     */
    public final Object getComponent(final String componentName) {
        if(!hasComponent(componentName)) {
            throw new NoSuchComponentRuntimeException(componentName);
        }
        // will fire firstGetComponent
        ComponentMeta componentMeta = components.get(componentName);
        boolean firstGet = componentMeta.isFirstGet();
        if(firstGet) { // first to use, may need to do @Inject for Fields&Methods
            Object component = componentMeta.getComponentObject();
            firstGetComponent(componentName, componentMeta.getComponentClass(), component);
        }
        
        if(containerAnnotation.useCGLIB()) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(componentMeta.getComponentClass());
            enhancer.setCallback(new MethodInterceptor(){
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return  invokeComponent(componentName, method, args);
                }
            });
            return enhancer.create();
        }
        else {
            // create Dynamic Proxy component
            return Proxy.newProxyInstance(this.getClass().getClassLoader(), componentMeta.getInterfaceClasses(), new java.lang.reflect.InvocationHandler(){
                public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
                    return invokeComponent(componentName, method, args);
                }
            });
        }
    }

    /**
     * invoke component method, InvocationHandlers chain will handle this invocation.
     */
    protected Object invokeComponent(String componentName, Method componentMethod, Object... params) throws Throwable {
        // fire event
        eventManager.fireComponentMethodInvokeEvent(new ComponentInvocationEvent(this, componentName, componentMethod, params, null, null));
        ComponentMeta componentMeta = components.get(componentName);
        boolean firstGet = componentMeta.isFirstGet();
        if(firstGet) { // first to use, may need to do @Inject for Fields&Methods
            Object component = componentMeta.getComponentObject();
            firstGetComponent(componentName, componentMeta.getComponentClass(), component);
        }
        
        try {
            Invocation invocation = createInvocation(componentName, componentMethod, params);
            invocation.invokeByChain(handlers.iterator());
            Object result = invocation.getResult();
            // fire event
            eventManager.fireComponentMethodReturnEvent(new ComponentInvocationEvent(this, componentName, componentMethod, params, result, null));
            return result;
        }
        catch (InvocationRuntimeException e) {
            log.error("Caught Exception while invoking " + componentMeta.getComponentClass().getName() + "." + componentMethod.getName(),e);
            // fire event
            eventManager.fireComponentMethodReturnEvent(new ComponentInvocationEvent(this, componentName, componentMethod, params, null, e.getCause()));
            throw e.getCause();
        }
    }

    protected Invocation createInvocation(String componentName, Method componentMethod, Object... params) {
        ComponentMeta componentMeta = components.get(componentName);
        return new Invocation(this, componentMeta, componentMeta.getComponentObject(), componentMethod, params);
    }

    // ---------- methods for resolving annotations
    
    protected void beforeConstructComponent(final String componentName, Class<?> componentClass){
        ComponentMeta componentMeta = components.get(componentName);
        ClassResolveContext classResolveContext = new ClassResolveContext(this, componentName, componentClass, null);
        for(Annotation classAnnotation : componentMeta.getClassAnnotations()){
            classResolveContext.setAnnotation(classAnnotation);
            annotationResolverRepository.resolveClassAnnotationAroundConstruct(classResolveContext, Resolve.WHEN.BEFORE_CONSTRUCT);
        }

        for(Map.Entry<Field, Annotation[]> entry : componentMeta.getFieldAnnotations().entrySet()) {
            Field field = entry.getKey();
            Annotation[] fieldAnnotations = entry.getValue();
            FieldResolveContext fieldResolveContext = new FieldResolveContext(this, componentName, componentClass, null, field);
            for(Annotation fieldAnnotation : fieldAnnotations){
                fieldResolveContext.setAnnotation(fieldAnnotation);
                annotationResolverRepository.resolveFieldAnnotationAroundConstruct(fieldResolveContext, Resolve.WHEN.BEFORE_CONSTRUCT);
            }
        }

        for(Map.Entry<Method, Annotation[]> entry : componentMeta.getMethodAnnotations().entrySet()) {
            Method method = entry.getKey();
            MethodResolveContext methodResolveContext = new MethodResolveContext(this, componentName, componentClass, null, method, null, null);
            for(Annotation methodAnnotation : entry.getValue()) {
                methodResolveContext.setAnnotation(methodAnnotation);
                annotationResolverRepository.resolveMethodAnnotationAroundConstruct(methodResolveContext, Resolve.WHEN.BEFORE_CONSTRUCT);
            }
        }
    }

    protected void afterConstructComponent(final String componentName, Class<?> componentClass, Object componentObj){
        ComponentMeta componentMeta = components.get(componentName);
        //only resolve class annotations, field annotation will delay to be resolved while getting 
        ClassResolveContext classResolveContext = new ClassResolveContext(this, componentName, componentClass, componentObj);
        for(Annotation classAnnotation : componentMeta.getClassAnnotations()){
            classResolveContext.setAnnotation(classAnnotation);
            annotationResolverRepository.resolveClassAnnotationAroundConstruct(classResolveContext, Resolve.WHEN.AFTER_CONSTRUCT);
        }
    }

    // resolve field&method annotation after first getComponent as Resolve.WHEN.AFTER_CONSTRUCT, to ensure succeed to inject Field&Method 
    protected void firstGetComponent(final String componentName, Class<?> componentClass, Object componentObj){
        ComponentMeta componentMeta = components.get(componentName);
        //resolve field annotations
        for(Map.Entry<Field, Annotation[]> entry : componentMeta.getFieldAnnotations().entrySet()) {
            Field field = entry.getKey();
            Annotation[] fieldAnnotations = entry.getValue();
            FieldResolveContext fieldResolveContext = new FieldResolveContext(this, componentName, componentClass, componentObj, field);
            for(Annotation fieldAnnotation : fieldAnnotations){
                fieldResolveContext.setAnnotation(fieldAnnotation);
                annotationResolverRepository.resolveFieldAnnotationAroundConstruct(fieldResolveContext, Resolve.WHEN.AFTER_CONSTRUCT);
            }            
        }

        //resolve method annotation after first get, such as @Inject
        for(Map.Entry<Method, Annotation[]> entry : componentMeta.getMethodAnnotations().entrySet()) {
            Method method = entry.getKey();
            MethodResolveContext methodResolveContext = new MethodResolveContext(this, componentName, componentClass, componentObj, method, null, null);
            for(Annotation methodAnnotation : entry.getValue()) {
                methodResolveContext.setAnnotation(methodAnnotation);
                annotationResolverRepository.resolveMethodAnnotationAroundConstruct(methodResolveContext, Resolve.WHEN.AFTER_CONSTRUCT);
            }
        }
    }
}